/**
 * 在 Web Worker 中运行 MQTT Client，将操作和事件以 Worker Message 的形式暴露出来
 */
import type * as MQTT_NS from 'mqtt'
// @ts-ignore 这个路径没有类型定义
import MQTTLib from 'mqtt/dist/mqtt'
import type { Packet } from 'mqtt-packet'
import { WorkerContext } from '@/lib/web-worker'

/**
 * 直接引 'mqtt' 包，它会访问 process、Buffer 变量以及尝试引入 `url` 包，但 web 环境下这些变量、包并不存在
 * v4 版本的 webpack 会对这些内容进行 ployfill，新版则不会
 *
 * 可以通过手动引入 polyfill 解决 process 和 Buffer 的问题，但 url 包好像没有好办法
 *
 * 按网上的资料，改为引入 'mqtt/dist/mqtt' 可解决此问题，但此文件没有类型定义
 * 因此要手动补足类型
 */
// @ts-ignore: 此文件没有类型定义
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace MQTT {
  export type ISubscriptionGrant = MQTT_NS.ISubscriptionGrant
  export type QoS = MQTT_NS.QoS
  export type Client = MQTT_NS.Client
  export const connect = (MQTTLib as typeof MQTT_NS).connect
}

/**
 * Worker 定义
 */

type ActionResult<T> =
  | { topic: string; success: true; info: T }
  | { topic: string; success: false; error: string }
export type SubscribeResult = ActionResult<MQTT.ISubscriptionGrant[]>
export type UnsubscribeResult = ActionResult<Packet | undefined>

export interface MQTTMessages {
  host: {
    connect: MQTTOptions
    subscribe: string // topic
    unsubscribe: string // topic
    publish: { topic: string; payload: string }
  }
  worker: {
    'status-change': { current: ConnStatus; prev: ConnStatus }
    'subscribe-result': SubscribeResult
    'unsubscribe-result': UnsubscribeResult
    message: { topic: string; payload: string }
  }
}

const worker = new WorkerContext<MQTTMessages['worker'], MQTTMessages['host']>()
worker.on('connect', connect)
worker.on('subscribe', subscribe)
worker.on('unsubscribe', unsubscribe)
worker.on('publish', publish)

/**
 * MQTT 操作
 */

export type ConnStatus = 'connecting' | 'reconnecting' | 'connected' | 'connectFailed'

export interface MQTTOptions {
  address: string
  connectTimeout?: number
  keepalive?: number
  qos?: MQTT.QoS
}

let client: MQTT.Client | undefined
let status: ConnStatus
let qos: MQTT.QoS = 1

function connect(options: MQTTOptions) {
  if (client) return

  const { address, connectTimeout = 15000, keepalive = 10 } = options
  if (typeof options.qos === 'number') qos = options.qos

  client = MQTT.connect(address, { connectTimeout, keepalive })

  setStatus('connecting')
  client.on('connect', () => setStatus('connected'))
  client.on('reconnect', () => setStatus('reconnecting'))
  client.on('close', () => {
    if (status === 'connecting' || status === 'reconnecting') setStatus('connectFailed')
  })
  client.on('message', handleMessage)
}

function setStatus(current: ConnStatus) {
  const prev = status
  status = current
  worker.post('status-change', { current, prev })
}

function handleMessage(topic: string, payload: Buffer) {
  worker.post('message', { topic, payload: payload.toString() })
}

function subscribe(topic: string) {
  client?.subscribe(topic, { qos }, (err: Error | null, info) => {
    if (err) worker.post('subscribe-result', { topic, success: false, error: String(err) })
    else worker.post('subscribe-result', { topic, success: true, info })
  })
}

function unsubscribe(topic: string) {
  client?.unsubscribe(topic, { qos }, (err?: Error, info?) => {
    if (err) worker.post('unsubscribe-result', { topic, success: false, error: String(err) })
    else worker.post('unsubscribe-result', { topic, success: true, info })
  })
}

function publish(msg: { topic: string; payload: string }) {
  client?.publish(msg.topic, msg.payload)
}
